Clipify
在视频中找到精彩瞬间,将其裁剪为独立的片段,可选择将 16:9 转换为 9:16 格式(面部平移或分屏),通过时间扭曲平移实现更紧凑的揭示效果,并添加 Opus 风格的逐字字幕。
模式
在步骤 1 之前选择一种模式。该选择会影响寻找精彩片段的方式以及是否应用字幕。
- 对话模式(默认用于播客、访谈、人物特写):使用 Whisper 进行转录,您需要扫描其中的精彩台词/反应/尴尬停顿。→ 执行步骤 1A。字幕在步骤 5 中添加。
- 动作模式(适用于砍柴、运动、打击乐、鼓点、任何有重复撞击声的场景):完全跳过 Whisper,运行
detect_transients.py来定位每次撞击,并在每次撞击处进行裁剪。→ 执行步骤 1B。跳过步骤 5;撞击声本身就是节奏。 - 混合模式:对对话部分使用对话模式,对剩余部分使用动作模式,穿插进行。同时使用 1A 和 1B。
如果用户说“这里我没说话”,则使用动作模式。如果他们说“在每次 X(砍、击、拳、鼓点、挥动、脚步)处进行裁剪”,则使用动作模式。否则默认使用对话模式。
输入
- 视频文件路径(用户需提供;否则请询问)
- 可选:目标格式(9:16、16:9、1:1)——如果未提供,请在选取候选片段后询问
- 可选:字幕样式偏好——如果未提供,请在添加字幕前询问
工具(仅使用最快路径)
- Whisper:
whisper --model tiny.en --word_timestamps True --output_format json(比small.en快约 10 倍;英语质量尚可)。对于非英语:使用--model base(去掉--language)。 - ffmpeg: 解码时添加
-hwaccel videotoolbox,渲染时添加-preset ultrafast。最终母版使用-c:v libx264 -crf 20。 - Numpy: 用于音频对齐(FFT 互相关)。无需 scipy/cv2。
- 脚本:
<skill-dir>/scripts/(其中<skill-dir>是包含此 SKILL.md 的目录——通常为~/.claude/skills/clipify/)analyze.py—— 从两个 ROI 运动文件生成说话者时间线build_pan.py—— 生成带硬切段的 ffmpeg 裁剪 x 表达式build_ass.py—— 从 whisper JSON 生成 Opus 风格的 ASS 字幕audio_align.py—— 查找子片段在较长源文件中的偏移量detect_transients.py—— 查找尖锐的撞击声(动作模式);参见--help
工作目录:/tmp/clipify/(开始时创建,保留工件以便调试)。
工作流程
步骤 1A —— 查找对话精彩时刻(对话模式)
mkdir -p /tmp/clipify
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
whisper /tmp/clipify/audio.wav --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
读取生成的 JSON(或 .txt)文件,并选取 3–5 个候选片段。需要扫描的搞笑信号:
- 精彩台词和反应: 诸如“什么”、“等等”、“不会吧”、笑声、“哈哈”、咒骂等词语
- 反转时刻: 铺垫性问题 → 意料之外的回答
- 尴尬停顿: Whisper 片段中带有长间隙,或填充词(“呃”、“嗯”)
- 自嘲 / 可引用的金句: 可独立存在的简短陈述句
- 音频峰值: 通过
ffmpeg -af volumedetect检测,或查找快速的来回对话(交替出现的简短 Whisper 片段)
对于每个候选片段,提出:[开始时间,结束时间,搞笑原因,建议标题]。目标片段时长 10–25 秒。展示列表,让用户确认/挑选。
步骤 1B —— 查找每次撞击(动作模式)
用音频瞬态检测器替代 Whisper。该检测器通过计算 1–6 kHz 频段的频谱通量来查找尖锐撞击声(斧头砍木、拳击垫子、鼓面、球棒击球、木材落地)——这些是撞击产生的宽带脉冲,环境/风声则没有。
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
python3 <skill-dir>/scripts/detect_transients.py /tmp/clipify/audio.wav \
--min-flux 30 --min-gap 0.6 > /tmp/clipify/strikes.json
调优参数:
--min-flux 30是合理的默认值。真实的撞击声在 50–300 之间;环境/风声在 5–15 之间。误报过多 → 提高到 40–50。命中过少 → 降低到 20。--min-gap 0.6(秒)是两次撞击之间的最小间隔。快速击鼓可能需要 0.2;砍柴使用 0.6–1.0 即可。--band 1000:6000涵盖木材/金属/玻璃撞击声。沉重的重击声(低音鼓、身体打击声)频率更低(--band 200:2000);口哨声/叮当声/军鼓声频率更高(--band 4000:10000)。- 对于多片段源(一个手机片段文件夹),在每个片段上运行检测器,并在不同角度选取多样化的片段集。对于时长 45–60 秒、每段约 0.9 秒的蒙太奇,目标选取 50–65 个撞击点。
每个“镜头”应在撞击前约 0.4 秒开始(显示蓄力动作),并在撞击后约 0.5 秒结束(撞击 + 短暂的后续动作)。即每个节拍约 0.9 秒。
步骤 1.5 —— 验证每个候选片段是否在画面内
在任何模式下,渲染之前务必执行此步骤。 音频检测只能找到事件的声音;相机可能并未捕捉到画面。跳过此步骤是用户反馈“你在我甚至没出现的时候进行了切割”的首要原因。
# 在每个候选撞击时间 T 处采样一帧:
for T in $STRIKE_TIMES; do
ffmpeg -y -ss "$T" -i "$VIDEO" -frames:v 1 -vf scale=320:-1 \
/tmp/clipify/verify_${T}.jpg
done
检查每张验证图像,并在以下情况下舍弃该候选片段:
- 拍摄对象弯腰、在画框外或位于障碍物后面
- 镜头上有拇指(是的,这会发生 —— 只有通过视觉才能发现)
- 明显是不同的音源(门砰地关上、掉落的工具)触发了检测器
对于约 30 个候选片段,这相当于一次快速的 ffmpeg 调用加上一次批量图像读取。成本低廉,但很值得。
步骤 2 —— 裁剪每个选定的片段
ffmpeg -y -ss "$START" -t "$DURATION" -i "$VIDEO" -c copy /tmp/clipify/clip_$N.mp4
(使用 -c copy 实现即时修剪。仅当裁剪需要帧精确时才重新编码。)
步骤 3 —— 决定输出格式
询问用户(如果他们已指定则跳过):“9:16(TikTok / Reels)、16:9(YouTube)还是 1:1(Instagram 信息流)?”
步骤 4 —— 如果 16:9 → 9:16:面部间平移 vs 分屏
使用 ffprobe 检测源素材宽高比。如果源素材为 16:9 且目标为 9:16,请询问:
“两种选择:(a) 硬切平移,跟随说话者(一次只显示一个面部),或 (b) 分屏,两个面部同时可见。您想要哪一种?”
如果只有一个面部(单说话者片段),则跳过该问题。对于单说话者,只需居中裁剪即可。
步骤 4a —— 面部间平移(推荐用于快速剪辑的对话人物特写)
-
定位两个面部 ROI。 采样一帧:
ffmpeg -ss <middle> -i <clip> -frames:v 1 /tmp/clipify/probe.jpg。读取它。目测每个面部的嘴巴+下巴区域,表示为源像素空间的x,y,w,h。(无需 cv2 —— 片段内相机静态,一帧足够。)通过绘制框来验证:ffmpeg -i probe.jpg -vf "drawbox=x=$LX:y=$LY:w=$LW:h=$LH:[email protected]:t=4,drawbox=x=$RX:y=$RY:w=$RW:h=$RH:[email protected]:t=4" verify.jpg最多迭代两次。框应覆盖嘴巴和下巴,并避免遮挡手和麦克风。不要过度调优 —— 帧差分具有容错性。
-
在每个 ROI 中提取逐帧运动能量:
ffmpeg -y -i clip.mp4 -filter_complex " [0:v]split=2[a][b]; [a]crop=$LW:$LH:$LX:$LY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/L.txt[la]; [b]crop=$RW:$RH:$RX:$RY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/R.txt[ra] " -map "[la]" -f null - -map "[ra]" -f null - -
构建说话者时间线(最小停留时间 1.0 秒 —— 短插话合并到前一个说话者):
python3 <skill-dir>/scripts/analyze.py /tmp/clipify/L.txt /tmp/clipify/R.txt 1.0 > /tmp/clipify/segments.json -
从源素材中选取 9:16 垂直条带的平移 x 坐标。 假设源素材宽度 W=1920,目标宽度 W=1080,裁剪条带宽度 = 608。
- LEFT_X =
face_left_center_x - 304(限制 ≥ 0) - RIGHT_X =
face_right_center_x - 304(限制 ≤ source_W - 608)
- LEFT_X =
-
生成硬切 x 表达式并渲染:
EXPR=$(python3 <skill-dir>/scripts/build_pan.py /tmp/clipify/segments.json $LEFT_X $RIGHT_X) ffmpeg -y -hwaccel videotoolbox -i clip.mp4 -filter_complex \ "[0:v]crop=608:1080:x='$EXPR':y=0,scale=1080:1920:flags=lanczos[v]" \ -map "[v]" -map 0:a -c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p \ -c:a aac -b:a 192k /tmp/clipify/clip_panned.mp4假设源为 1920×1080;对于 4K 源,要么先降低分辨率,要么将所有坐标加倍。
步骤 4b —— 分屏(两个面部始终可见)
两个堆叠的瓷砖,每个 1080×960。当前说话者的瓷砖位于顶部——在说话者切换时翻转叠加。
[0:v]split=2[a0][a1];
[a0]crop=Wcrop:Hcrop:LX_tile:LY_tile,scale=1080:960,split=2[lt0][lt1];
[a1]crop=Wcrop:Hcrop:RX_tile:RY_tile,scale=1080:960,split=2[rt0][rt1];
[lt0][rt0]vstack[layoutL];
[rt1][lt1]vstack[layoutR];
[layoutL][layoutR]overlay=0:0:enable='<RIGHT_SPEAKER_ENABLE>'[v]
从 segments.json 构建 <RIGHT_SPEAKER_ENABLE>,作为右侧说话者片段的 between(t,a,b)+between(t,a,b)+...。瓷砖裁剪应瞄准每个面部周围约 720×640 的区域(1.125:1 以匹配 1080×960)。
步骤 4c —— 时间扭曲揭示效果(可选)
当用户想要收紧一个缓慢的风景/平移/揭示镜头(“把它减半”)但又不失去其弧线时,使用速度渐变而不是修剪。修剪迫使你放弃部分弧线;速度渐变则以更高的速度保留所有节拍。
# 2 倍速度:12 秒弧线 → 6 秒输出,通过 atempo 保持音频音高
ffmpeg -y -ss "$START" -i "$CLIP" -t "$ORIG_DUR" \
-vf "setpts=PTS/2,scale=1080:1920:flags=lanczos,fps=30" \
-af "atempo=2.0" \
-c:v libx264 -preset fast -crf 21 -pix_fmt yuv420p \
-c:a aac -b:a 128k /tmp/clipify/clip_2x.mp4
经验法则:
- 1.5x —— 人类运动应感觉“稍微轻快”,但看起来不像是被加速
- 2x —— 更有力的揭示效果;仍然感觉自然
- 3x–4x —— 延时摄影氛围(使用
atempo=2.0,atempo=2.0链实现 4x;每个滤镜实例atempo仅接受 0.5–2.0) - 如果音频只是环境/风声,并且花栗鼠音效会分散注意力,则去掉
-af atempo并静音该段
当剪辑的其他部分具有节奏感(砍柴蒙太奇、对话来回切换),而揭示效果可能会像是冷场时,此功能非常有用。
步骤 5 —— 添加字幕
在动作模式下跳过此步骤——撞击声就是节奏,字幕只会使画面杂乱。
仅询问一次(仅当用户尚未指定样式时):
“三种字幕样式:opus(大号粗体白色,黄色活动词高亮)、卡拉OK(4 个词一组,绿色高亮)、极简(简洁的 Helvetica,无高亮)。或者粘贴您喜欢的示例。”
如果他们粘贴了参考图像/示例:尽可能匹配字体、字号、粗细、颜色、位置和动画——手动编写自定义 ASS 或扩展 build_ass.py。
否则使用预设:
# 在修剪后的片段上重新运行 whisper,以获得相对于片段开始的准确时间戳
whisper /tmp/clipify/clip_panned.mp4 --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
python3 <skill-dir>/scripts/build_ass.py /tmp/clipify/clip_panned.json /tmp/clipify/captions.ass opus
烧录字幕:
ffmpeg -y -i /tmp/clipify/clip_panned.mp4 -vf "subtitles=/tmp/clipify/captions.ass" \
-c:v libx264 -preset fast -crf 20 -c:a copy "$OUTPUT.mp4"
步骤 6 —— 交付
- 将每个输出保存到
<source_dir>/clipify_out/(如果目录不存在则创建) - 每行打印一个片段:名称、时长、搞笑原因、输出路径
- 使用
open <path>打开第一个输出,以便用户检查 - 提供迭代选项(不同样式、不同 ROI、切换到分屏、调整字幕时间)
陷阱(以往运行中的经验教训——请勿重蹈覆辙)
- 音频检测 ≠ 可见事件。 在渲染之前,务必执行步骤 1.5(验证每个候选帧)。检测器找到的是砍劈的声音,而不是砍劈者是否在画面内。拍摄对象弯腰、镜头外或镜头上有拇指的镜头仍然会触发音频检测器。在渲染之前发现它们。
- 频谱通量频段很重要。 默认的
--band 1000:6000涵盖木材/金属/玻璃撞击声。沉重的低音重击声(低音鼓、身体打击声)需要使用--band 200:2000。口哨声、叮当声、军鼓声需要使用--band 4000:10000。如果--min-flux 30没有返回任何结果,首先尝试降低到 15;如果返回了数千个结果,则在提高阈值之前尝试不同的频段。 - 成对进行速度渐变音频。 在单个滤镜中,
atempo仅接受 0.5–2.0;对于 4 倍速,使用atempo=2.0,atempo=2.0链。如果没有atempo,仅使用setpts会导致花栗鼠音效。 - 不要过度调优 ROI。 最多迭代两次。运动差分具有容错性——覆盖嘴巴和下巴的更宽 ROI 即使不是完美地对准嘴巴中心也能正常工作。
- 注意片段内的场景切换。 运行
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -来计算切换次数。如果 16:9→9:16 的片段有很多切换,固定的面部 ROI 仅适用于主导场景;警告用户,并提供要么选择一个单次连续拍摄的片段,要么接受在切换期间偏离中心的取景。 - 源分辨率很重要。 如果源是 4K,要么首先降低到 1920×1080(更快,对于 9:16 输出来说足够好),要么将所有 ROI/平移坐标乘以 2。
- 源素材中已烧录的字幕。 有些“原始”剪辑仍然带有字幕。如果是这样,通过音频互相关(
audio_align.py)找到无字幕的母版,并从那里进行修剪。 - 如果短片段足够,则不要在整个时长很长的源文件上运行 whisper。 在步骤 2 之后对修剪后的片段运行 whisper;仅在步骤 1 中当你需要转录文本来寻找搞笑时刻时,才在完整的源文件上运行 whisper。
- 用一句话说明计划,然后执行。 不要叙述每一次迭代。