cang-ying/video-ai-system/tools/local_motion.py

71 lines
3.0 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""local_motion.py — 本地运镜工具BC-006 IMAGE-FIRST 默认路由 LOCAL_MOTION
静态图 ffmpeg zoompan 竖屏 9:16 运镜片段¥0 视频模型费
用法:
python tools/local_motion.py <图片文件夹> -o <输出文件夹> [--dur 6] [--fps 24]
运镜模式循环: push_in / zoom_out / pan_left / pan_right / pan_up / static
输出: <输出文件夹>/shot_01.mp4 ... 每个片段 dur
"""
import os, sys, subprocess, argparse, glob
def normalize(fps):
return ["-vf", f"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,scale=4320:7680,fps={fps}"]
def zoompan_filter(mode, frames, fps):
z = "z='min(1+0.0008*on,1.6)'" # push_in 推近
x = "x='iw/2-(iw/zoom/2)'"
y = "y='ih/2-(ih/zoom/2)'"
if mode == "zoom_out":
z = "z='max(1.5-0.0008*on,1.0)'"
elif mode == "pan_left": # 从左扫到右
z = "z='1.2'"; x = "x='(iw-iw/zoom)*(on/{})'".format(frames)
elif mode == "pan_right": # 从右扫到左
z = "z='1.2'"; x = "x='(iw-iw/zoom)*(1-on/{})'".format(frames)
elif mode == "pan_up": # 从下往上
z = "z='1.2'"; y = "y='(ih-ih/zoom)*(1-on/{})'".format(frames)
elif mode == "static":
z = "z='1.0'"
return f"zoompan={z}:{x}:{y}:d={frames}:s=1080x1920:fps={fps}"
def make_shot(img, out, dur, fps, mode):
frames = dur * fps
vf = normalize(fps)[1] + "," + zoompan_filter(mode, frames, fps)
cmd = ["ffmpeg", "-y", "-loop", "1", "-i", img, "-vf", vf,
"-t", str(dur), "-r", str(fps), "-c:v", "libx264", "-preset", "fast",
"-crf", "20", "-pix_fmt", "yuv420p", "-an", out]
r = subprocess.run(cmd, capture_output=True, text=True)
return r.returncode == 0, r.stderr[-200:] if r.returncode else ""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("folder")
ap.add_argument("-o", "--output", default="local_motion_out")
ap.add_argument("--dur", type=int, default=6)
ap.add_argument("--fps", type=int, default=24)
args = ap.parse_args()
imgs = sorted(glob.glob(os.path.join(args.folder, "*.png")) +
glob.glob(os.path.join(args.folder, "*.jpg")) +
glob.glob(os.path.join(args.folder, "*.jpeg")) +
glob.glob(os.path.join(args.folder, "*.webp")))
if not imgs:
print("❌ 文件夹里没有图片"); return 1
os.makedirs(args.output, exist_ok=True)
modes = ["push_in", "zoom_out", "pan_left", "pan_right", "pan_up", "static"]
ok = 0
for i, img in enumerate(imgs[:64], 1):
mode = modes[(i - 1) % len(modes)]
out = os.path.join(args.output, f"shot_{i:02d}.mp4")
print(f"[{i}/{len(imgs)}] {os.path.basename(img)} -> {mode} ...", end=" ")
good, err = make_shot(img, out, args.dur, args.fps, mode)
if good:
print(""); ok += 1
else:
print("", err[-120:])
print(f"完成: {ok}/{len(imgs)} 个片段已生成 -> {args.output}")
if __name__ == "__main__":
sys.exit(main())