Communitygithub.com

layyyback/tennis-rally-cut

把一段很长的网球练球视频(对墙练、发球机、对拉都行)按击球声自动剪成一个个完整回合,去掉捡球、走动、停顿。当用户说「帮我把练球视频剪成一段段」「把打球的部分剪出来」「去掉捡球」「按回合切网球视频」,或给出一个网球视频目录要求剪辑时使用。也适用于乒乓球、羽毛球、壁球等击球声清楚的练习视频。

tennis-rally-cut란 무엇인가요?

tennis-rally-cut 是一个基于音频分析的智能视频剪辑工具,专为网球训练视频设计(也适用于乒乓球、羽毛球、壁球等)。它通过检测击球声(球拍击球声 + 球撞墙/落地声)来自动识别并切割出每一个完整的回合,彻底去除捡球、走动、停顿等无关片段。核心优势在于“不剪碎回合”——通过可调的静音间隔阈值(默认6秒),确保连续多拍的回合不会被错误分割成多个短片段。该技能依赖 ffmpeg、ffprobe 和 numpy,提供完整的工作流:probe 检测画质(防止微信等软件压缩导致的画质问题)、analyze 通过音频检测击球事件并聚类成回合、sheets 生成每回合的首尾帧截图供人工核查(必须步骤)、cut 按回合剪辑成独立 MP4 文件,并自动备份旧输出。最终输出清晰的片段清单和验收报告,且原视频始终不被修改。

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/layyyback/tennis-rally-cut/tree/HEAD/tennis-rally-cut

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

网球回合剪辑(tennis-rally-cut)

沉淀自 2026-11-08 的一次真实剪辑:两段对墙练球视频,剪出 14 个回合。 核心思路:听声音找回合,看画面做核对。

用户要的是什么(硬规则)

  • 一个回合 = 从第一拍到球打死。整个回合放进一个片段。
  • 不许把回合剪碎。 连续对墙打了 10 拍,就是一个片段,不能拆成几个 2 拍、3 拍。
  • 捡球、走动、停顿、拿手机、收机器,都剪掉。
  • 原视频不动。成品放在原视频同目录的 剪辑/ 文件夹。

为什么用声音不用画面

  • 每一拍都有「拍子击球」和「球撞墙/落地」两声脆响。捡球、走路时没有。
  • 画面动静不行:捡球走动时画面变化比打球还大,分不出来。
  • 画面只用来抽查核对。

工具

脚本:scripts/rally_cut.py。只依赖 ffmpeg、ffprobe 和 numpy。

子命令作用
probe看分辨率、码率、方向;提示是否被聊天软件压缩
analyze找击球声,串成回合,写 <名字>.segments.json
sheets每个回合开头、结尾各拼一张帧图,用来核对
cut按回合剪成独立 mp4,写 片段清单.txt
align换了高清原片时,算新旧两版的时间偏移

下文 RC 指脚本的绝对路径。中间文件放会话的临时目录,不要放进用户的视频目录。

流程

1. 先查画质

python3 -I $RC probe <视频...>
  • 出现「疑似被聊天软件压缩」时,先告诉用户。剪辑本身几乎不损失画质,原片糊成品就糊。
  • 微信传来的视频,原片常在微信缓存里,文件名同名加 _raw 后缀: ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/<账号>/msg/video/<年-月>/<名字>_raw.mp4 用户在微信里点「查看原视频」之后才会下载。可用 mdfind 按加入时间搜: mdfind -onlyin ~ 'kMDItemContentTypeTree == "public.movie" && kMDItemDateAdded >= $time.iso(<日期>)'
  • 找到高清原片后,用 align 确认偏移为 0,就能直接沿用已经算好的切点。

2. 找回合

python3 -I $RC analyze <视频...> --work <临时目录>

它会打印每秒击球声数量的时间条和回合列表。默认参数:

参数默认含义
--k2.5比背景响几倍算一声。环境吵就调大
--gap6两声间隔超过 6 秒才算回合结束。这是「不剪碎」的关键
--min-hits6少于 6 声的串当噪声丢掉(脚步、收手机)
--pre / --post1 / 1.5回合前后多留的秒数,保证挥拍和落点完整

--gap 的取舍:

  • 4 秒会把「球死后几秒内立刻重新开球」拆成两段。
  • 6 秒把它们留在一起。用户要求不剪碎,所以默认 6。
  • 用户明确说「每次重新开球都单独一段」时,才改成 4。

3. 看画面核对(不可跳过)

python3 -I $RC sheets <视频> --segments <json> --work <临时目录>/sheets

逐张看图,重点查:

  • 视频最后一个回合:结尾常有人走向镜头、拿起手机,声音会被当成击球。看到就把 end 改到最后一拍后约 1.5 秒。
  • 只有个位数击球声的短回合:可能是噪声。画面里没人在挥拍就删掉。
  • 回合之间 5 秒左右的空档:确认是球死重开,而不是漏检。

直接编辑 segments.json 里的 segments 数组:改 start/end,或删整条。

4. 剪

python3 -I $RC cut <视频> --segments <json> --out <视频目录>/剪辑 --label A
  • 多个源视频用不同 --label(A、B…),文件名形如 视频A_03_2m54s-3m22s.mp4,时间是原片中的起止。
  • 必须重新编码(默认 H.264、CRF 17)。原片关键帧常隔 2~3 秒,直接拷贝会切偏。
  • 竖拍视频的方向信息 ffmpeg 会自动处理,成品就是正的。
  • 目标目录已有旧成品时,先改名成 剪辑_旧版 备份,不要直接覆盖或删除。
  • 1080p 60 帧的片子编码较慢,放后台跑。

5. 验收再汇报

  • 用 ffprobe 核每个成品的时长、分辨率、帧率。
  • 从一两个成品里截帧看一眼:方向正、画面清楚。
  • 汇报时给:片段数、每段时长范围、成品目录(附 Finder 能点开的 file:// 链接,中文路径要百分号编码)。
  • 写明拿不准的片段(例如击球声很少的短回合),让用户自己决定删不删。
  • 写明只抽查了画面,没有逐段从头看到尾。

踩过的坑

坑后果做法
循环里调 ffmpeg 没加 -nostdinffmpeg 吃掉清单后续行,切点错乱、崩溃脚本已加
时间按「格数×10ms」换算一格其实是 220 个采样点,7 分钟漂 1 秒脚本按采样点换算
用画面动静判断打球捡球比打球动得还厉害只用声音判断
用压缩过的转发视频成品发糊,用户以为是剪坏了第 1 步先 probe

관련 스킬