Created
May 8, 2026 00:05
-
-
Save taroyanaka/96dc4e17c74b5746e2e85c69aaccb996 to your computer and use it in GitHub Desktop.
Python 3.10+, moviepy==2.2.1, ffmpeg, 実行例: python split_mirror.py -i input.mp4 -o output.mp4 --stop 3 5 9 --side right
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import argparse | |
| import os | |
| import sys | |
| import subprocess | |
| from moviepy import ( | |
| VideoFileClip, | |
| ImageClip, | |
| clips_array, | |
| concatenate_videoclips, | |
| vfx | |
| ) | |
| # --------------------------------------- | |
| # ffmpeg encoder 自動判定 | |
| # --------------------------------------- | |
| def get_available_encoders(): | |
| try: | |
| result = subprocess.run( | |
| ["ffmpeg", "-encoders"], | |
| capture_output=True, | |
| text=True | |
| ) | |
| return result.stdout | |
| except Exception: | |
| return "" | |
| def choose_codec(): | |
| encoders = get_available_encoders() | |
| if "h264_nvenc" in encoders: | |
| print("GPU Encoder: h264_nvenc") | |
| return "h264_nvenc" | |
| elif "h264_qsv" in encoders: | |
| print("GPU Encoder: h264_qsv") | |
| return "h264_qsv" | |
| elif "h264_amf" in encoders: | |
| print("GPU Encoder: h264_amf") | |
| return "h264_amf" | |
| print("CPU Encoder: libx264") | |
| return "libx264" | |
| # --------------------------------------- | |
| # moviepy compatibility | |
| # --------------------------------------- | |
| def safe_subclip(clip, start, end): | |
| if hasattr(clip, "subclipped"): | |
| return clip.subclipped(start, end) | |
| return clip.subclip(start, end) | |
| # --------------------------------------- | |
| # freeze section | |
| # --------------------------------------- | |
| def freeze_segment(clip, start, end): | |
| """ | |
| start-end 区間を静止画化 | |
| """ | |
| # freeze開始時点のフレーム取得 | |
| frame = clip.get_frame(start) | |
| frozen = ImageClip(frame).with_duration( | |
| end - start | |
| ) | |
| if hasattr(clip, "fps"): | |
| frozen = frozen.with_fps( | |
| clip.fps | |
| ) | |
| return frozen | |
| # --------------------------------------- | |
| # build alternating freeze clip | |
| # --------------------------------------- | |
| def build_alternating_clip( | |
| original_clip, | |
| stop_times, | |
| freeze_on_even | |
| ): | |
| """ | |
| freeze_on_even=True: | |
| 偶数区間で停止 | |
| freeze_on_even=False: | |
| 奇数区間で停止 | |
| """ | |
| duration = original_clip.duration | |
| # 区間点生成 | |
| points = [0] | |
| points.extend(stop_times) | |
| points.append(duration) | |
| segments = [] | |
| for i in range(len(points) - 1): | |
| start = points[i] | |
| end = points[i + 1] | |
| should_freeze = ( | |
| i % 2 == 0 | |
| if freeze_on_even | |
| else i % 2 == 1 | |
| ) | |
| if should_freeze: | |
| print( | |
| f"FREEZE {start:.2f} - {end:.2f}" | |
| ) | |
| seg = freeze_segment( | |
| original_clip, | |
| start, | |
| end | |
| ) | |
| else: | |
| print( | |
| f"PLAY {start:.2f} - {end:.2f}" | |
| ) | |
| seg = safe_subclip( | |
| original_clip, | |
| start, | |
| end | |
| ) | |
| segments.append(seg) | |
| return concatenate_videoclips( | |
| segments | |
| ) | |
| # --------------------------------------- | |
| # main | |
| # --------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="左右交互フリーズミラー動画" | |
| ) | |
| parser.add_argument( | |
| "-i", | |
| "--input", | |
| required=True | |
| ) | |
| parser.add_argument( | |
| "-o", | |
| "--output", | |
| default="output.mp4" | |
| ) | |
| parser.add_argument( | |
| "--side", | |
| choices=["left", "right"], | |
| default="right" | |
| ) | |
| parser.add_argument( | |
| "--stop", | |
| type=float, | |
| nargs="+", | |
| required=True | |
| ) | |
| args = parser.parse_args() | |
| if not os.path.exists(args.input): | |
| print("入力動画が見つかりません") | |
| sys.exit(1) | |
| clip = None | |
| final_clip = None | |
| try: | |
| print("----- 処理開始 -----") | |
| clip = VideoFileClip(args.input) | |
| w, h = clip.size | |
| duration = clip.duration | |
| half_w = w // 2 | |
| print(f"解像度: {w}x{h}") | |
| print(f"長さ: {duration:.2f}秒") | |
| # --------------------------------------- | |
| # 左右ベース生成 | |
| # --------------------------------------- | |
| if args.side == "left": | |
| print("左側基準") | |
| left_base = clip.with_effects([ | |
| vfx.Crop( | |
| x1=0, | |
| y1=0, | |
| x2=half_w, | |
| y2=h | |
| ) | |
| ]) | |
| try: | |
| right_base = left_base.with_effects([ | |
| vfx.MirrorX() | |
| ]) | |
| except Exception: | |
| right_base = left_base.with_effects([ | |
| vfx.MirrorHorizontal() | |
| ]) | |
| else: | |
| print("右側基準") | |
| right_base = clip.with_effects([ | |
| vfx.Crop( | |
| x1=w-half_w, | |
| y1=0, | |
| x2=w, | |
| y2=h | |
| ) | |
| ]) | |
| try: | |
| left_base = right_base.with_effects([ | |
| vfx.MirrorX() | |
| ]) | |
| except Exception: | |
| left_base = right_base.with_effects([ | |
| vfx.MirrorHorizontal() | |
| ]) | |
| # stop整理 | |
| stops = sorted([ | |
| s for s in args.stop | |
| if 0 < s < duration | |
| ]) | |
| print("停止ポイント:") | |
| print(stops) | |
| # --------------------------------------- | |
| # 左右交互フリーズ | |
| # --------------------------------------- | |
| # 左: | |
| # 偶数区間 再生 | |
| # 奇数区間 停止 | |
| left_final = build_alternating_clip( | |
| left_base, | |
| stops, | |
| freeze_on_even=False | |
| ) | |
| # 右: | |
| # 偶数区間 停止 | |
| # 奇数区間 再生 | |
| right_final = build_alternating_clip( | |
| right_base, | |
| stops, | |
| freeze_on_even=True | |
| ) | |
| # --------------------------------------- | |
| # 左右結合 | |
| # --------------------------------------- | |
| final_clip = clips_array([ | |
| [left_final, right_final] | |
| ]) | |
| # 音声 | |
| if clip.audio: | |
| final_clip = final_clip.with_audio( | |
| clip.audio | |
| ) | |
| # codec | |
| codec = choose_codec() | |
| print(f"使用codec: {codec}") | |
| # --------------------------------------- | |
| # 書き出し | |
| # --------------------------------------- | |
| print("レンダリング開始") | |
| final_clip.write_videofile( | |
| args.output, | |
| codec=codec, | |
| audio_codec="aac", | |
| bitrate="5000k", | |
| threads=os.cpu_count(), | |
| fps=clip.fps if hasattr(clip, "fps") else 30, | |
| preset="medium" | |
| ) | |
| print("----- 完了 -----") | |
| except Exception as e: | |
| print("実行エラー:") | |
| print(e) | |
| import traceback | |
| traceback.print_exc() | |
| finally: | |
| try: | |
| if final_clip: | |
| final_clip.close() | |
| if clip: | |
| clip.close() | |
| except Exception: | |
| pass | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment