Skip to content

Instantly share code, notes, and snippets.

@taroyanaka
Created May 8, 2026 00:05
Show Gist options
  • Select an option

  • Save taroyanaka/96dc4e17c74b5746e2e85c69aaccb996 to your computer and use it in GitHub Desktop.

Select an option

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
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