Communitygithub.com

layyyback/tennis-rally-cut

Automatically splits long tennis practice videos (wall drills, ball machines, rallies) into complete rally clips by detecting hit sounds, removing ball retrieval, walking, and pauses.

What is tennis-rally-cut?

This skill, `tennis-rally-cut`, is designed for developers and users who need to automatically segment long tennis practice videos into individual rally clips. It works by analyzing audio to detect distinct hit sounds (racket-ball contact, ball-wall/ground bounces) and then grouping consecutive hits into rallies, removing all non-playing moments like ball retrieval, walking, and rest. The skill supports tennis, table tennis, badminton, and squash videos where hit sounds are clear, and it includes robust tools for quality checking, manual verification via frame sheets, and final cutting with re-encoding to ensure accurate clip boundaries. It uses a Python script with ffmpeg and numpy, and its workflow is structured: probe to check quality, analyze to find rallies, sheets to visually verify cut points, and cut to create final clips. A key design principle is never to split a rally across multiple clips; the default gap of 6 seconds between hits ensures rallies are kept intact unless the user explicitly requests tighter cuts. This tool is built on real-world experience from a November 2026 editing session where two wall-drill videos were successfully cut into 14 rallies.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/layyyback/tennis-rally-cut/tree/HEAD/tennis-rally-cut

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Tennis Rally Cut (tennis-rally-cut)

Distilled from a real editing session on 2026-11-08: two wall-drill videos, cut into 14 rallies. Core idea: Use sound to find rallies, use video to verify.

What the User Wants (Hard Rules)

  • A rally = from the first hit until the ball is dead. The entire rally goes into one clip.
  • Never split a rally. If someone hits 10 consecutive shots against a wall, that's one clip; do not break it into several 2- or 3-shot pieces.
  • Ball retrieval, walking, pauses, picking up a phone, grabbing equipment — all removed.
  • Original videos remain untouched. Output goes into a 剪辑/ folder in the same directory as the original video.

Why Use Sound, Not Visuals

  • Every hit produces two crisp sounds: "racket hitting ball" and "ball hitting wall/ground." No such sounds during ball pickup or walking.
  • Visual motion doesn't work: walking and reaching for the ball create more motion than playing.
  • Video is only used for spot-checking and verification.

Tools

Script: scripts/rally_cut.py. Only depends on ffmpeg, ffprobe, and numpy.

SubcommandPurpose
probeCheck resolution, bitrate, orientation; warn if compressed by chat apps
analyzeDetect hit sounds, stitch into rallies, write <name>.segments.json
sheetsGenerate frame sheets (start and end frame of each rally) for visual review
cutCut by rally into separate mp4s, write 片段清单.txt
alignWhen a high-quality original is available, compute time offset between old and new versions

Hereafter, RC refers to the absolute path of the script. Intermediate files go into a session temp directory, not into the user's video directory.

Workflow

1. Check Quality First

python3 -I $RC probe <video...>
  • If it says "suspected compression by chat app," inform the user first. Editing itself hardly reduces quality; if the original is blurry, the output will be too.
  • Videos from WeChat often have the original in the WeChat cache, with filename suffix _raw: ~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/<account>/msg/video/<yyyy-mm>/<name>_raw.mp4 The user must tap "View Original" in WeChat before it downloads. Use mdfind to search by date added: mdfind -onlyin ~ 'kMDItemContentTypeTree == "public.movie" && kMDItemDateAdded >= $time.iso(<date>)'
  • Once the high-quality original is found, use align to confirm offset is 0, then the computed cut points can be reused directly.

2. Find Rallies

python3 -I $RC analyze <video...> --work <temp_dir>

It prints a timeline of hits per second and a rally list. Default parameters:

ParameterDefaultMeaning
--k2.5How many times louder than background to count as a hit. Increase in noisy environments
--gap6If two hits are more than 6 seconds apart, it's the end of a rally. This is key to not splitting rallies.
--min-hits6Sequences with fewer than 6 hits are discarded as noise (footsteps, phone pickups)
--pre / --post1 / 1.5Seconds to pad before and after each rally to ensure swing and landing are complete

Trade-off for --gap:

  • 4 seconds would split scenarios where "ball is dead but player restarts within a few seconds" into two clips.
  • 6 seconds keeps them together. The user requires not splitting, so default is 6.
  • Only change to 4 if the user explicitly says "each restart is a separate clip."

3. Visual Verification (Must Not Skip)

python3 -I $RC sheets <video> --segments <json> --work <temp_dir>/sheets

Look at each frame sheet, focus on:

  • The last rally of the video: often ends with someone walking toward the camera or picking up a phone, whose sounds can be mistaken for hits. If you see this, change end to about 1.5 seconds after the last real hit.
  • Short rallies with only a few hits: could be noise. Delete if nobody is swinging in the frame.
  • Gaps of about 5 seconds between rallies: confirm they are dead-ball restarts, not missed detections.

Edit the segments array in segments.json directly: change start/end, or remove the entire segment.

4. Cut

python3 -I $RC cut <video> --segments <json> --out <video_dir>/剪辑 --label A
  • Multiple source videos use different --label (A, B…), file names like 视频A_03_2m54s-3m22s.mp4, where time is from the original.
  • Must re-encode (default H.264, CRF 17). Original keyframes are often 2–3 seconds apart; direct copy would misalign cuts.
  • For portrait videos, ffmpeg will automatically handle rotation; output will be upright.
  • If the target directory already has old output, rename them to 剪辑_old_version as backup — do not overwrite or delete.
  • 1080p 60fps encoding is slow; run it in the background.

5. Verify, Then Report

  • Use ffprobe to check each output's duration, resolution, and frame rate.
  • Extract a frame from one or two outputs: check orientation and clarity.
  • Report: number of clips, duration range per clip, output directory (provide a file:// link that Finder can open; percent-encode Chinese paths).
  • Flag any questionable clips (e.g., very short rallies with few hits) so the user can decide whether to delete them.
  • State that you only spot-checked the visuals, not watched every clip in full.

Pitfalls Encountered

PitfallConsequenceFix
Calling ffmpeg in a loop without -nostdinffmpeg ate subsequent lines from the pipeline, causing wrong cut points and crashesScript now includes it
Time calculated as "frames × 10ms"One frame is actually 220 samples; drifts by 1 second over 7 minutesScript converts by samples
Using visual motion to detect playingPicking up the ball creates more motion than playingUse sound only
Using a compressed forwarded videoOutput is blurry; user thinks editing ruined itStep 1: probe first

Related Skills