テニスラリー分割(tennis-rally-cut)
2026年11月8日の実際の編集セッションから得られた知見を基にしています。2本の壁打ち練習動画から14のラリーを切り出しました。 中心となる考え方:音でラリーを見つけ、映像で確認する。
ユーザーの要件(厳守ルール)
- 1ラリー = 最初の打撃からボールが止まるまで。ラリー全体を1つのクリップに収める。
- ラリーを分割してはいけない。 壁打ちで10打連続したら、それが1クリップであり、2打、3打ずつに分割してはいけない。
- 球拾い、歩行、停止、スマホ操作、機器の片付けは全てカットする。
- 元の動画は変更しない。成果物は元動画と同じディレクトリの
編集/フォルダに保存する。
音声を使う理由(映像ではない理由)
- 各打撃には「ラケットでボールを打つ音」と「ボールが壁/地面に当たる音」の2つの明確な音がある。球拾いや歩行中にはこれらの音は発生しない。
- 映像の動きの大きさは頼りにならない:球拾いや歩行時の映像の変化は打球時よりも激しいため、区別がつかない。
- 映像は確認のためにのみ使用する。
ツール
スクリプト: scripts/rally_cut.py。依存関係は ffmpeg、ffprobe、numpy のみ。
| サブコマンド | 機能 |
|---|---|
probe | 解像度、ビットレート、向きを確認。SNSアプリで圧縮されていないか警告。 |
analyze | 打球音を検出し、ラリーにまとめ、<名前>.segments.json を生成。 |
sheets | 各ラリーの開始と終了フレームの画像を生成し、目視確認を支援。 |
cut | ラリーごとに独立したmp4ファイルに切り出し、片断清单.txt を生成。 |
align | 高画質の原版に差し替えた際、新旧の時間オフセットを計算。 |
以降、RC はスクリプトの絶対パスを指します。中間ファイルはセッションの一時ディレクトリに配置し、ユーザーの動画ディレクトリには置かないでください。
ワークフロー
1. まず画質を確認
python3 -I $RC probe <動画...>
- 「SNSで圧縮された可能性があります」と表示された場合、その旨をユーザーに通知する。編集処理自体は画質をほとんど劣化させないが、元の動画がぼやけていれば出力もぼやける。
- WeChat から送られてきた動画の場合、オリジナルはWeChatのキャッシュにあり、ファイル名に
_rawが付加されることが多い:~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/<アカウント>/msg/video/<年-月>/<名前>_raw.mp4ユーザーがWeChatで「元の動画を見る」をタップした後にダウンロードされる。mdfindで追加日時から検索可能:mdfind -onlyin ~ 'kMDItemContentTypeTree == "public.movie" && kMDItemDateAdded >= $time.iso(<日付>)' - 高画質の原版を見つけたら、
alignでオフセットが0であることを確認し、既に計算済みのカットポイントをそのまま流用できる。
2. ラリーを見つける
python3 -I $RC analyze <動画...> --work <一時ディレクトリ>
1秒あたりの打球音の数の時間経過バーとラリーのリストを出力する。デフォルトパラメータ:
| パラメータ | デフォルト値 | 説明 |
|---|---|---|
--k | 2.5 | 背景音より何倍大きければ打球音とみなすか。環境が騒がしい場合は大きくする。 |
--gap | 6 | 2つの打球音の間隔が何秒以上ならラリー終了とみなすか。「分割禁止」の鍵。 |
--min-hits | 6 | これより少ない打撃数の連続はノイズとして破棄(足音、スマホの取り出しなど)。 |
--pre / --post | 1 / 1.5 | ラリーの前後に追加で残す秒数。スイングや着地点が欠けないようにする。 |
--gap のトレードオフ:
- 4秒:ボールが止まった後、数秒以内にすぐに次の打球を始めた場合、それを2つのラリーに分割してしまう。
- 6秒:それらを1つのラリーにまとめる。ユーザーは分割禁止を要求しているので、デフォルトは6秒。
- ユーザーが「新しいラリーは毎回別のクリップにして」と明示した場合のみ、4秒に変更する。
3. 映像で確認(必須ステップ)
python3 -I $RC sheets <動画> --segments <json> --work <一時ディレクトリ>/sheets
画像を1枚ずつ確認し、特に以下をチェック:
- 動画の最後のラリー: 終了時に人がカメラに向かって歩いたりスマホを手に取る動作があり、その音が打球音として誤検出されることがある。見つけたら、
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 60fpsの動画はエンコードに時間がかかるため、バックグラウンドで実行する。
5. 確認と報告
- ffprobe で各成果物の長さ、解像度、フレームレートを確認。
- 1〜2つの成果物からフレームを切り出し、向きが正しく、映像が鮮明であることを確認。
- 報告時には:クリップ数、各クリップの長さの範囲、成果物ディレクトリ(Finderで開ける
file://リンクを添付。日本語パスはパーセントエンコードすること)。 - 確信が持てないクリップ(例:打球音が非常に少ない短いラリー)については、ユーザー自身に削除を判断してもらうよう明記する。
- 映像は抜き打ちで確認したものであり、すべてのクリップを最初から最後まで見たわけではないことを明記する。
経験済みの落とし穴
| 落とし穴 | 結果 | 対策 |
|---|---|---|
ループ内でffmpegを呼び出す際に -nostdin を付けなかった | ffmpegがリストの後続行を読み込み、カット位置がずれたりクラッシュした | スクリプトに追加済み |
| 時間を「フレーム数×10ms」で換算した | 1フレームが実際には220サンプルであり、7分で1秒ずれる | スクリプトではサンプルポイントで換算 |
| 映像の動きの大きさで打球を判断した | 球拾いの方が打球よりも動きが激しかった | 音声のみで判断 |
| 圧縮された転送動画を使用した | 出力がぼやけ、ユーザーが編集ミスだと思った | ステップ1でプローブする |